﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;

using DShowNET;
using DShowNET.Device;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.IO;
using System.Drawing.Imaging;

namespace WebCamControl
{
    public partial class WebCamControl : UserControl, ISampleGrabberCB
	{
		public enum PlayState : int
		{
			Stopped,
			Paused,
			Running,
			Init
		}

		#region DirectShow related attributes
		private PlayState CurrentState = PlayState.Stopped;

		private int WM_GRAPHNOTIFY = Convert.ToInt32("0X8000", 16) + 1;
        private const int WS_CHILD = 0x40000000;	// attributes for video window
        private const int WS_CLIPCHILDREN = 0x02000000;
        private const int WS_CLIPSIBLINGS = 0x04000000;

		private IVideoWindow videoWindow = null;
		private IMediaControl mediaControl = null;
		private ISampleGrabber sampGrabber;
		private IBaseFilter baseGrabFlt;
		private VideoInfoHeader videoInfoHeader;

		private IMediaEventEx mediaEventEx = null;
		private IGraphBuilder graphBuilder = null;
		private ICaptureGraphBuilder2 captureGraphBuilder = null;

		private DsDevice activeDevice = null;

		/// <summary> buffer for bitmap data. </summary>
		private bool captured = true;
		//		private int bufferedSize;		
		private byte[] savedArray;
		#endregion

		private Timer timer4MemoryFlush = new Timer();
        private Timer eventEvokeTimer = new Timer();
        private bool isManualGrab = false;

        private Bitmap lastFrame;
        public Bitmap LastFrame
        {
            get { return lastFrame; }
            set { lastFrame = value; }
        }

        #region EventRelated
        public delegate void NewFrameHandler(object myObject, WebCamControlArgs myArgs);
        public event NewFrameHandler OnNewFrame;
        
        void eventEvokeTimer_Tick(object sender, EventArgs e)
        {
            if (CurrentState == PlayState.Running)
                videoGrabImage(false);

            eventEvokeTimer.Start();
        }
        #endregion

        #region VideoWriter Related
        private AVIWriter writer = null;
        private const int frameRate = 25;
        private bool recording = false;
        private string codec = string.Empty;
        #endregion

		public WebCamControl()
		{
            timer4MemoryFlush.Interval = 500;

            timer4MemoryFlush.Tick += new EventHandler(timer4MemoryFlush_Tick);
            timer4MemoryFlush.Start();

            eventEvokeTimer.Interval = 40;
            eventEvokeTimer.Tick += new EventHandler(eventEvokeTimer_Tick);
            eventEvokeTimer.Start();
			
			InitializeComponent();
		}

        void timer4MemoryFlush_Tick(object sender, EventArgs e)
        {
            MemoryManagement.FlushMemory();
            timer4MemoryFlush.Start();
        }

		private void WebCamControl_Load(object sender, System.EventArgs e)
		{
			this.Resize += new System.EventHandler(WebCamControl_Resize);
			//CaptureVideo();
		}

		private void InitializeComponent()
		{
			components = new System.ComponentModel.Container();
			this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
			System.Resources.ResourceManager resources = new System.Resources.ResourceManager(typeof(WebCamControl));

			this.Load += new System.EventHandler(WebCamControl_Load);
		}

        /// <summary>
        /// Начать получение видеопотока с камеры.
        /// !! Не означает начало записи. Просто транслирует изображение на панель.
        /// </summary>
		public void CaptureVideo()
		{
			int hr = 0;
			IBaseFilter sourceFilter = null;

			//IBaseFilter mux = null;
			//IFileSinkFilter sink = null;

            Guid pCategory;
            Guid pType;

			 try
			{
				// create the necessary DirectShow interfaces
				GetInterfaces();

				hr = this.captureGraphBuilder.SetFiltergraph(this.graphBuilder);
				Marshal.ThrowExceptionForHR(hr);

				sourceFilter = FindCaptureDevice();
				
				hr = this.graphBuilder.AddFilter(sourceFilter, "WebCamControl Video");				
				Marshal.ThrowExceptionForHR(hr);

				#region PICTURE

				AMMediaType media = new AMMediaType()
				{
					majorType = MediaType.Video,
					subType = MediaSubType.RGB24,
					formatType = FormatType.VideoInfo
				};
				hr = sampGrabber.SetMediaType(media);

				if (hr < 0)
					Marshal.ThrowExceptionForHR(hr);

				hr = graphBuilder.AddFilter(baseGrabFlt, "Ds.NET Grabber");
				if (hr < 0)
					Marshal.ThrowExceptionForHR(hr);

                pCategory = PinCategory.Capture;
                pType = MediaType.Video;
				hr = captureGraphBuilder.RenderStream(ref pCategory, ref pType, sourceFilter, null, baseGrabFlt); // baseGrabFlt 
				if (hr < 0)
					Marshal.ThrowExceptionForHR(hr);

				media = new AMMediaType();
				hr = sampGrabber.GetConnectedMediaType(media);
				if (hr < 0)
					Marshal.ThrowExceptionForHR(hr);
				if ((media.formatType != FormatType.VideoInfo) || (media.formatPtr == IntPtr.Zero))
					throw new NotSupportedException("Unknown Grabber Media Format");

				videoInfoHeader = (VideoInfoHeader)Marshal.PtrToStructure(media.formatPtr, typeof(VideoInfoHeader));
				Marshal.FreeCoTaskMem(media.formatPtr);
				media.formatPtr = IntPtr.Zero;

				////hr = sampGrabber.SetBufferSamples(false);
				////if (hr == 0)
				////    hr = sampGrabber.SetOneShot(false);
				////if (hr == 0)
				////    hr = sampGrabber.SetCallback(null, 0);
				////if (hr < 0)
				////    Marshal.ThrowExceptionForHR(hr);

				//// ~pic 

				#endregion

				#region VIDEO                

                //pType = MediaSubType.Avi;
                //hr = captureGraphBuilder.SetOutputFileName(ref pType, @"D:\file.avi", out mux, out sink);
                //if (hr < 0)
                //    Marshal.ThrowExceptionForHR(hr);

                //pType = MediaType.Video;
                //hr = captureGraphBuilder.RenderStream(ref pCategory, ref pType, sourceFilter, null, mux); // stream to file 
                //if (hr < 0)
                //    Marshal.ThrowExceptionForHR(hr);
                
				#endregion

				#region PREVIEW
				
                pCategory = PinCategory.Preview;
				hr = this.captureGraphBuilder.RenderStream(ref pCategory, ref pType, sourceFilter, null, null);
				Marshal.ThrowExceptionForHR(hr);
				
				#endregion

				SetupVideoWindow();

				hr = this.mediaControl.Run();
				Marshal.ThrowExceptionForHR(hr);				

				this.CurrentState = PlayState.Running;

				MemoryManagement.FlushMemory();
			}
			catch (Exception ex)
			{
				 MessageBox.Show("An unrecoverable error has occurred.\r\n" + ex.ToString());
			}
			finally
			{
				if (sourceFilter != null)
					Marshal.ReleaseComObject(sourceFilter);
                //if (mux != null)
                //    Marshal.ReleaseComObject(mux); mux = null;
                //if (sink != null)
                //    Marshal.ReleaseComObject(sink); sink = null;
			}
		}

        private void GetInterfaces()
        {
            Type comType = null;
            object comObj = null;
            try
            {
                comType = Type.GetTypeFromCLSID(Clsid.FilterGraph);
                if (comType == null)
                    throw new NotImplementedException(@"DirectShow FilterGraph not installed/registered!");
                comObj = Activator.CreateInstance(comType);
                graphBuilder = (IGraphBuilder)comObj; comObj = null;

                Guid clsid = Clsid.CaptureGraphBuilder2;
                Guid riid = typeof(ICaptureGraphBuilder2).GUID;
                comObj = DsBugWO.CreateDsInstance(ref clsid, ref riid);
                captureGraphBuilder = (ICaptureGraphBuilder2)comObj; comObj = null;

                comType = Type.GetTypeFromCLSID(Clsid.SampleGrabber);
                if (comType == null)
                    throw new NotImplementedException(@"DirectShow SampleGrabber not installed/registered!");
                comObj = Activator.CreateInstance(comType);
                sampGrabber = (ISampleGrabber)comObj; comObj = null;

                mediaControl = (IMediaControl)graphBuilder;
                videoWindow = (IVideoWindow)graphBuilder;
                mediaEventEx = (IMediaEventEx)graphBuilder;
                baseGrabFlt = (IBaseFilter)sampGrabber;
            }
            catch (Exception ee)
            {
                MessageBox.Show(this, "Could not get interfaces\r\n" + ee.Message, "DirectShow.NET", MessageBoxButtons.OK, MessageBoxIcon.Stop);
            }
            finally
            {
                if (comObj != null)
                    Marshal.ReleaseComObject(comObj); comObj = null;
            }
        }

		private IBaseFilter FindCaptureDevice()
		{
			#region commented
			/* 
		    UCOMIEnumMoniker classEnum = null;
            UCOMIMoniker[] moniker = new UCOMIMoniker[1];
            object source = null;

            ICreateDevEnum devEnum = (ICreateDevEnum)(new CreateDevEnum());
            int hr = devEnum.CreateClassEnumerator(FilterCategory.VideoInputDevice, out classEnum, CDef.None);
            Marshal.ThrowExceptionForHR(hr);
            Marshal.ReleaseComObject(devEnum);

			//DsDevice.GetDevicesOfCat(

            if (classEnum == null)
                throw new ApplicationException("No video capture device was detected.\\r\\n\\r\\n" + 
					"This application requires a video capture device, such as a USB WebCam,\\r\\nto be installed and working properly." +
					"\nThe application will now close.");

            int none = 0;
			
            if (classEnum.Next(moniker.Length, moniker, out none) == 0)
            {
                Guid iid = typeof(IBaseFilter).GUID;
                moniker[0].BindToObject(null, null, ref iid, out source);
            }
            else
            {
                throw new ApplicationException("Unable to access video capture device!");
            }

            Marshal.ReleaseComObject(moniker[0]);
            Marshal.ReleaseComObject(classEnum);

            return (IBaseFilter)source;
		    */
			/*
			DsDevice dev = null;
			if (capDevices.Count == 1)
				dev = capDevices[0] as DsDevice;
			else
			{
				DeviceSelector selector = new DeviceSelector(capDevices);
				selector.ShowDialog(this);
				dev = selector.SelectedDevice;
			}
			*/
			#endregion

			if (activeDevice == null)
			{
                System.Collections.ArrayList devices = null;
                DsDev.GetDevicesOfCat(FilterCategory.VideoInputDevice, out devices);
				if (devices.Count < 1)
					throw new Exception("No video capture devices found!");

				DsDevice dev = null;
				if (devices.Count == 1)
					dev = devices[0] as DsDevice;
				else
				{
					DeviceSelector selector = new DeviceSelector(System.Collections.ArrayList.Adapter(devices));
					selector.ShowDialog();
					dev = selector.SelectedDevice;
				}

				this.activeDevice = dev;
			}

			Guid iid = typeof(IBaseFilter).GUID;
			object source;
			activeDevice.Mon.BindToObject(null, null, ref iid, out source);

			return (IBaseFilter)source;
		}

		private void SetupVideoWindow()
		{
			int hr = 0;

			//set the video window to be a child of the main window
			//putowner : Sets the owning parent window for the video playback window. 
			hr = this.videoWindow.put_Owner(this.Handle);
			Marshal.ThrowExceptionForHR(hr);

            hr = this.videoWindow.put_WindowStyle(WS_CHILD | WS_CLIPCHILDREN);
			Marshal.ThrowExceptionForHR(hr);

			//Use helper function to position video window in client rect of main application window
			WebCamControl_Resize(this, null);

			//Make the video window visible, now that it is properly positioned
			//put_visible : This method changes the visibility of the video window. 
            hr = this.videoWindow.put_Visible(DsHlp.OATRUE);
			Marshal.ThrowExceptionForHR(hr);
		}

		//protected override void WndProc(ref Message m)
		//{
		//    if (m.Msg == WM_GRAPHNOTIFY)
		//    {
		//        HandleGraphEvent();
		//    }
		//    if (this.videoWindow != null)
		//    {
		//        this.videoWindow.NotifyOwnerMessage(m.HWnd, m.Msg, m.WParam.ToInt32(), m.LParam.ToInt32());
		//    }
		//    base.WndProc(ref m);
		//}

		//private void HandleGraphEvent()
		//{
		//    int hr = 0;
		//    EventCode evCode = 0;
		//    int evParam1 = 0;
		//    int evParam2 = 0;

		//    //this.Refresh();

		//    while (this.mediaEventEx != null && this.mediaEventEx.GetEvent(out evCode, out evParam1, out evParam2, 0) == 0)
		//    {
		//        // Free event parameters to prevent memory leaks associated with
		//        // event parameter data.  While this application is not interested
		//        // in the received events, applications should always process them.
		//        hr = this.mediaEventEx.FreeEventParams(evCode, evParam1, evParam2);
		//        Marshal.ThrowExceptionForHR(hr);

		//        // Insert event processing code here, if desired (see http://msdn2.microsoft.com/en-us/library/ms783649.aspx)
		//    }
		//}       

		private void ReleaseInterfaces()
		{
			timer4MemoryFlush.Stop();

            if (writer != null)
            {
                recording = false;
                writer.Close();
                writer.Dispose();
                writer = null;
            }

			if (this.mediaControl != null)
				this.mediaControl.StopWhenReady();

			this.CurrentState = PlayState.Stopped;

			// stop notifications of events
			if (this.mediaEventEx != null)
				this.mediaEventEx.SetNotifyWindow(IntPtr.Zero, WM_GRAPHNOTIFY, IntPtr.Zero);

			//// Relinquish ownership (IMPORTANT!) of the video window.
			//// Failing to call put_Owner can lead to assert failures within
			//// the video renderer, as it still assumes that it has a valid
			//// parent window.
			if (this.videoWindow != null)
			{
                this.videoWindow.put_Visible(DsHlp.OAFALSE);
				this.videoWindow.put_Owner(IntPtr.Zero);
			}

			// Release DirectShow interfaces
			Marshal.ReleaseComObject(this.mediaControl);
			this.mediaControl = null;

			Marshal.ReleaseComObject(this.mediaEventEx);
			this.mediaEventEx = null;

			Marshal.ReleaseComObject(this.videoWindow);
			this.videoWindow = null;

			Marshal.ReleaseComObject(this.graphBuilder);
			this.graphBuilder = null;
            
			Marshal.ReleaseComObject(this.captureGraphBuilder);
			this.captureGraphBuilder = null;

			MemoryManagement.FlushMemory();
		}

		private void WebCamControl_Resize(object sender, System.EventArgs e)
		{
			//Resize the video preview window to match owner window size
			if (this.videoWindow != null)
				this.videoWindow.SetWindowPosition(0, 0, this.Width, this.ClientSize.Height);
		}

        /// <summary>
        /// Освободить интерфейсы, прекратить транслировать видео.
        /// </summary>
        public void StopCapture()
        {
            ReleaseInterfaces();
        }

		#region Захват изображения
		private delegate void CaptureDone();

		int ISampleGrabberCB.SampleCB(double SampleTime, IMediaSample pSample)
		{
			Trace.WriteLine("!!CB: ISampleGrabberCB.SampleCB");
			return 0;
		}

		/// <summary> buffer callback, COULD BE FROM FOREIGN THREAD. </summary>
		int ISampleGrabberCB.BufferCB(double SampleTime, IntPtr pBuffer, int BufferLen)
		{
			if (captured || (savedArray == null))
			{
				Trace.WriteLine("!!CB: ISampleGrabberCB.BufferCB");
				return 0;
			}

			captured = true;
			//bufferedSize = BufferLen;
			Trace.WriteLine("!!CB: ISampleGrabberCB.BufferCB  !GRAB! size = " + BufferLen.ToString());
			if ((pBuffer != IntPtr.Zero) && (BufferLen > 1000) && (BufferLen <= savedArray.Length))
				Marshal.Copy(pBuffer, savedArray, 0, BufferLen);
			else
				Trace.WriteLine("    !!!GRAB! failed ");
			this.BeginInvoke(new CaptureDone(this.OnCaptureDone));
			return 0;
		}

        /// <summary>
        /// Сохранить картинку с видеопотока в файл
        /// </summary>
        public void GrabImage()
        {
            this.eventEvokeTimer.Stop();
            videoGrabImage(true);
            this.eventEvokeTimer.Start();
        }

		private void videoGrabImage(bool isManual)
		{
            this.isManualGrab = isManual;

			int hr;
			if (sampGrabber == null)
				return;

			Trace.WriteLine("!!BTN: toolBarBtnGrab");

			if (savedArray == null)
			{
				int size = videoInfoHeader.BmiHeader.ImageSize; // 921600
				if ((size < 1000) || (size > 16000000))
					return;
				savedArray = new byte[size + 64000];
			}

			captured = false;
			hr = sampGrabber.SetCallback(this, 1);
			return;
		}

		void OnCaptureDone()
		{
			try
			{
				int hr;
				if (sampGrabber == null)
					return;
				hr = sampGrabber.SetCallback(null, 0);

				int w = videoInfoHeader.BmiHeader.Width; // 640
				int h = videoInfoHeader.BmiHeader.Height; // 480
				if (((w & 0x03) != 0) || (w < 32) || (w > 4096) || (h < 32) || (h > 4096))
					return;
				int stride = w * 3;

				GCHandle handle = GCHandle.Alloc(savedArray, GCHandleType.Pinned);
				int scan0 = (int)handle.AddrOfPinnedObject();
				scan0 += (h - 1) * stride;

				Bitmap b = new Bitmap(w, h, -stride, PixelFormat.Format24bppRgb, (IntPtr)scan0);
				handle.Free();
				savedArray = null;

                lastFrame = b;

                if (isManualGrab)
                {
                    string fileName = String.Format("Pic {0}-{1}-{2}.JPG", DateTime.Now.ToShortDateString(),
                                DateTime.Now.ToLongTimeString(), DateTime.Now.Millisecond).Replace(':', '.');
                    fileName = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), fileName);
                    b.Save(fileName, ImageFormat.Jpeg); 
                }
                else
                {
                    // Throw event!!!      
                    OnNewFrame(this, new WebCamControlArgs(b, b.Width, b.Height));
                }
                isManualGrab = false;

                if (recording && writer != null)
                {
                    writer.AddFrame(b);
                }

				MemoryManagement.FlushMemory();
			}
			catch (Exception ee)
			{
				MessageBox.Show(this, "Could not grab picture\r\n" + ee.Message, "DirectShow.NET", MessageBoxButtons.OK, MessageBoxIcon.Stop);
			}
			return;
		}
		#endregion

        #region Захват видео
        /// <summary>
        /// Начать запись видео с выбранным кодеком
        /// </summary>
        /// <param name="codecName">Название кодека в виде строки. Например "wmv3".</param>
        public void RecordVideoStart(string codecName)
        {
            this.codec = codecName;

            this.RecordVideoStart();
        }

        /// <summary>
        /// Начать запись видео в файл. Запись осуществляется без сжатия.
        /// </summary>
        public void RecordVideoStart()
        {
            if (writer == null)
            {
                // create file name
                DateTime date = DateTime.Now;
                String fileName = String.Format("{0}-{1}-{2} {3}-{4}-{5}.avi",
                    date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second);

                try
                {
                    // create AVI writer
                    if (codec == string.Empty)
                        writer = new AVIWriter();
                    else
                    {
                        writer = new AVIWriter(codec);
                        codec = string.Empty;
                    }
                    // open AVI file
                    writer.Open(fileName, lastFrame.Width, lastFrame.Height);
                    writer.FrameRate = frameRate;
                }
                catch (ApplicationException ex)
                {
                    if (writer != null)
                    {
                        writer.Dispose();
                        writer = null;
                    }
                }
            }
            recording = true;
        }

        /// <summary>
        /// Остановить запись видео в файл
        /// </summary>
        public void RecordVideoStop()
        {
            if (writer != null)
            {
                recording = false;
                writer.Close();
                writer.Dispose();
                writer = null;
            }
        }
        #endregion
	}
}